// Copyright 2012 The Glue BSF Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
 * @fileoverview glue Model implementation and cache abstraction. 
 */

goog.provide('glue.Model');

goog.require('glue.Cache');

goog.require('goog.array');
goog.require('goog.async.Deferred');
goog.require('goog.events.EventTarget');
goog.require('goog.json');
goog.require('goog.net.XhrManager');
goog.require('goog.string');

/** 
 * Creates the interface for dealing the model of the application.
 * 
 * @param {Object=} opt_headers Headers to include along with every XHR request.
 *  
 * @constructor
 * @extends goog.events.EventTarget
 */
glue.Model = function(opt_headers){
	
	var headers = {		
		'Accept':'application/json; charset=utf-8',
		'Content-Type':'application/json; charset=utf-8'
	}
	if(goog.isDefAndNotNull(opt_headers)){
		goog.object.extend(headers,opt_headers);
	}
		
	/** @type {goog.netXhrManager} Connection Manager */
	this.request_ = new goog.net.XhrManager(1,headers);
	
	/** @type {glue.Cache} CacheManager */
	//goog.bind(this.post,this,'/v3/util/cache/')
	this.cache_ = new glue.Cache(this);	
	this.logics_ = [];
	this.headers_ = headers;
};
goog.inherits(glue.Model,goog.events.EventTarget)

/**
 * Sets and fires the cache validation function.
 * 
 * @param {Function} a function that takes 1 parameter 
 *     this model and removes any items from cache that are invalid.
 */
glue.Model.prototype.setCacheValidation = function(func){
	this.cache_.setCacheValidation(func)
	this.cache_.revalidate();
}

/**
 * Retrieves arbitrary data from a URL on the same domain.
 * 
 * @param {string} method Retrieve method
 * @param {string} url URL of server resource
 * @param {Object=} opt_data JSON encodable data to send to server
 * @param {function(Object)=} opt_cb optional call back
 * 
 * @return {goog.async.Deferred} The Deferred object associated request.  
 *     It works like a Promise system.  It will be updated as the new XHR Closure 
 *     Promise system is rolled out.
 * 
 * @private
 */
glue.Model.prototype.retrieve_ = function(method,url,opt_data,opt_cb){	
	var id = url;	
	/** @type {{hasFired:boolean,deferred:{null|goog.async.Deferred}}}  */
	var returnValue = {hasFired:false,deferred:null};
	
	if(method === 'GET' && opt_data === undefined && this.cache_.containsKey(id)){		
		if(goog.isFunction(opt_cb)){
			/** @type */
			var result = this.cache_.get(id);	
			if(result instanceof goog.async.Deferred){				
				result.addCallback(opt_cb);
				returnValue.deferred = result;
			}else{				
				opt_cb(result);
				returnValue.hasFired = true;
			}
		}
	}else{		
		var datastring;
		if(opt_data !== undefined){
			datastring = JSON.stringify(opt_data);			
		}
		var deferred = this.cache_.setHolder(url,method === 'GET');
		returnValue.deferred = deferred;
		
		var request = this.request_.send(method+id,url,method,datastring,undefined,undefined,goog.bind(function(e){			
			var responce = e.target;	
						
			if(responce.isSuccess()){
				var rawData = responce.getResponseText();
				
				if(responce.getResponseHeader('Content-Type') === 'application/json' || goog.string.contains(url,'.json')){
					var /** @type {Object|array} */ data = goog.json.unsafeParse(rawData);	
					if(method !== 'GET'){
						goog.array.forEach(this.logics_,function(logic){
							logic.revalidate(method,url,opt_data,data)
						});	
					}
					this.handleData_(method==="GET"?id:undefined,data,deferred,opt_cb);	
					
				}else{
					if(goog.isFunction(opt_cb)){
						deferred.addCallback(opt_cb);
					}
					deferred.callback(rawData);	
				}
			}
		},this));	
	}
	return returnValue;
};

/**
 * Handles the response from the model.
 * 
 * @param {string} id ID of the request object
 * @param {string} rawData string of the JSON Object
 * @param {goog.async.Deferred} deferred The promise for the request. 
 * @param {function(Object)=} opt_cb callback to execute on the data 
 * 
 * @private
 */
glue.Model.prototype.handleData_ = function(id,data,deferred,opt_cb){
	if(id){
		this.cache_.set(id,data);
		data = this.cache_.get(id);	
	}
	if(goog.isFunction(opt_cb)){
		deferred.addCallback(opt_cb);
	}
	deferred.callback(data);			
};

/**
 * Function for listing the current cache. This is solely for debugging and 
 * should never be used to access the data.  This function needs to be annotated 
 * with something like never access except from the js console.  
 * 
 * For Example:
 *    lms.debug.list()
 * 
 * It was originally wrapped in a goog.DEBUG guard statement to remove it in production code 
 * but that can not be used as such here because some people might not use this with 
 * the Closure Library and a precompiled version has been created.
 * 
 * @export 
 * @public
 */
glue.Model.prototype.list = function(){	
	return this.cache_.toObject();	
}

/**
 * Function for clearing the current cache.
 * 
 * @export 
 * @public
 */
glue.Model.prototype.clear = function(){
	this.cache_.clear();		
}

/**
 * Sets the cached url to the data.
 * 
 * @param {String} url 
 * @param {*} data
 * 
 * @export 
 * @public
 */
glue.Model.prototype.set = function(url,data){
	this.cache_.set(url,data);
}

/**
 * Get Facade
 * 
 * @export
 * @public
 * 
 * @param {string} url Url of the resource to get
 * @param {Function=} opt_cb Callback function
 * 
 * @return {{hasFired:boolean,deferred:{null|goog.async.Deferred}}} 
 */
glue.Model.prototype.get = function(url,opt_cb){
	return this.retrieve_('GET',url,undefined,opt_cb);
}

/**
 * Post Facade
 * 
 * @export
 * @public
 * 
 * @param {string} url Url of the resource to post to
 * @param {*=} opt_data Data to be posted
 * @param {Function=} opt_cb Callback function
 * 
 * @return {{hasFired:boolean,deferred:{null|goog.async.Deferred}}} 
 */
glue.Model.prototype.post = function(url,opt_data,opt_cb){
	return this.retrieve_('POST',url,opt_data,opt_cb);
}

/**
 * Put Facade
 * 
 * @export
 * @public
 * 
 * @param {string} url Url of the resource to put
 * @param {*=} opt_data Data to be put
 * @param {Function=} opt_cb Callback function 
 * 
 * @return {{hasFired:boolean,deferred:{null|goog.async.Deferred}}}
 */
glue.Model.prototype.put = function(url,opt_data,opt_cb){
	return this.retrieve_('PUT',url,opt_data,opt_cb);
}

/**
 * Delete Facade
 * 
 * @export
 * @public
 * 
 * @param {string} url Url of the resource to delete
 * @param {*=} opt_data Data to be passed along with the delete
 * @param {Function=} opt_cb Callback function 
 * 
 * @return {{hasFired:boolean,deferred:{null|goog.async.Deferred}}}
 */
glue.Model.prototype.del = function(url,opt_data,opt_cb){
	return this.retrieve_('DELETE',url,opt_data,opt_cb);
}



/**
 * Adds a function that will be called on all returned data
 * 
 * @export
 * @public
 * 
 * @param {function(Object):Object} add a function to be called on retrieval of data.
 */
glue.Model.prototype.addLogic = function(logicObject){
	var logic = new logicObject(this);
	this.cache_.addLogic(logic);
	this.logics_.push(logic)
};

/**
 * Encapsulation of the cache revalidate function
 * 
 * @export
 * @public
 * 
 * @param {string} url URL to revalidate
 */
glue.Model.prototype.revalidate = function(url){
	this.cache_.revalidate(url);
}

/**
 * Function that will check to see if an url exists 
 * if the url exists fires the call back with the data as the only parameter.  
 * The call back should return the modified object.
 * Once the call back is finished the REVALIDATE event is fired 
 * updating any views visible that require the new data.
 * 
 * @param {String} url url of the cached item to check for.
 * @param {Function} func function with 1 parameter that is the data object and returns the modified data.
 * 
 * @export
 * @public
 */
glue.Model.prototype.conditionalRevalidate = function(url,func){
	this.cache_.conditionalRevalidate(url,func)
}

/**
 * Synchronous method for sending data to the server.  
 * This function should only be used on program exiting.
 * 
 * @param {String} method method(GET,POST,DELETE,PUT) of the call.
 * @param {String} url Url of the resource.
 * @param {*=} opt_data data to send along with the request.
 * 
 * @export
 * @public
 */
glue.Model.prototype.syncSend = function(method,url,opt_data){
	xhr = goog.net.XmlHttp();
	xhr.open(method,url,false);
	
	goog.object.forEach(this.headers_,function(header,key){
		xhr.setRequestHeader(key,header);
	})
	
	if(opt_data !== undefined && method === 'POST'){
		xhr.send(JSON.stringify(opt_data));	
	}else{
		xhr.send();
	}
}
